﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using MongoDAO.Exceptions;
using MongoDAO.Providers;
using MongoDB.Bson;
using MongoDB.Driver;
using MongoDB.Driver.Builders;
using MongoDB.Driver.Linq;

namespace MongoDAO.Repository
{
    public class Repository<T> : IRepository<T> where T : TDocument
    {
        protected readonly MongoCollection<T> Collection;
        protected readonly MongoDatabase Database;

        public Repository() : this (new MongoDatabaseProvider().GetDatabase())
        {
        }

        public Repository(MongoDatabase database)
        {
            Database = database;
            var collectionProvider = new MongoCollectionProvider(database);
            Collection = collectionProvider.GetCollection<T>();
        }

        public T Insert(T obj)
        {
            try
            {
                if (obj == null) throw new ArgumentNullException("obj");
                Collection.Insert(obj);
            }
            catch (WriteConcernException ex)
            {
                var code = ex.CommandResult.Code;
                var errorMessage = ex.CommandResult.Response.ToString();
                if (code == 11000) throw new DuplicatedKeyException(errorMessage);
            }
            return obj;
        }

        public IEnumerable<T> InsertBatch(IEnumerable<T> objects)
        {
            var insertBatch = objects as T[] ?? objects.ToArray();
            Collection.InsertBatch(insertBatch);
            return insertBatch;
        }

        public T Save(T obj)
        {
            try
            {
                if (obj == null) throw new ArgumentNullException("obj");
                Collection.Save(obj);
                return obj;
            }
            catch (WriteConcernException ex)
            {
                var code = ex.CommandResult.Code;
                var errorMessage = ex.CommandResult.Response.ToString();
                if (code == 11001 || code == 11000)
                    throw new DuplicatedKeyException(errorMessage);
                else
                    throw;
            }
        }

        public IEnumerable<T> FindAll()
        {
            return Collection.FindAll();
        }

        public T FindOneById(object id)
        {
            if (id == null) throw new ArgumentNullException("id");
            return Collection.FindOneById(BsonValue.Create(id));
        }

        public T FindOne(IMongoQuery query)
        {
            if (query == null) throw new ArgumentNullException("query");
            return Collection.FindOne(query);
        }

        public T FindOne(Expression<Func<T, bool>> expression)
        {
            if (expression == null) throw new ArgumentNullException("expression");
            return Collection.AsQueryable().Where(expression).FirstOrDefault();
        }

        public IEnumerable<T> Find(IMongoQuery query)
        {
            if (query == null) throw new ArgumentNullException("query");
            return Collection.Find(query);
        }

        public IEnumerable<T> Find(IMongoQuery query, int skip, int limit)
        {
            if (query == null) throw new ArgumentNullException("query");
            return Collection.Find(query).SetSkip(skip).SetLimit(limit);
        }

        public IQueryable<T> Find(Expression<Func<T, bool>> expression)
        {
            if (expression == null) throw new ArgumentNullException("expression");
            return Collection.AsQueryable().Where(expression);
        }


        public IEnumerable<T> Find(Expression<Func<T, bool>> expression, int skip, int limit)
        {
            if (expression == null) throw new ArgumentNullException("expression");
            return Collection.AsQueryable().Where(expression).Skip(skip).Take(limit);
        }


        public T FindAndModifyById(object id, IMongoUpdate updateBuilder)
        {
            try
            {
                if (id == null) throw new ArgumentNullException("id");
                var result = Collection.FindAndModify(Query<T>.EQ(x => x.Id, id), SortBy.Null, updateBuilder, true);
                var obj = result.GetModifiedDocumentAs<T>();
                return obj;
            }
            catch (MongoCommandException ex)
            {
                var code = ex.CommandResult.Code;
                var errorMessage = ex.CommandResult.Response.ToString();
                if (code == 11001) 
                    throw new DuplicatedKeyException(errorMessage);
                else
                    throw;
            }
        }

        public void Update(T obj)
        {
            try
            {
                Collection.Update(Query<T>.EQ(x => x.Id, obj.Id), Update<T>.Replace(obj));
            }
            catch (WriteConcernException ex)
            {
                var code = ex.CommandResult.Code;
                var errorMessage = ex.CommandResult.Response.ToString();
                if (code == 11001)
                    throw new DuplicatedKeyException(errorMessage);
                else
                    throw;
            }
        }

        public void Update(IMongoQuery query, IMongoUpdate updateBuilder)
        {
            try
            {
                Collection.Update(query, updateBuilder, UpdateFlags.Multi);
            }
            catch (WriteConcernException ex)
            {
                var code = ex.CommandResult.Code;
                var errorMessage = ex.CommandResult.Response.ToString();
                if (code == 11001) throw new DuplicatedKeyException(errorMessage);
            }
        }

        public void UpdateById(object id, IMongoUpdate updateBuilder)
        {
            try
            {
                Collection.Update(Query<T>.EQ(x => x.Id, id), updateBuilder);
            }
            catch (WriteConcernException ex)
            {
                var code = ex.CommandResult.Code;
                var errorMessage = ex.CommandResult.Response.ToString();
                if (code == 11001) throw new DuplicatedKeyException(errorMessage);
            }
        }


        public void RemoveAll()
        {
            Collection.RemoveAll();
        }

        public void Remove(IMongoQuery query)
        {
            Collection.Remove(query);
        }

        public void RemoveById(object Id)
        {
            Collection.Remove(Query<T>.EQ(x => x.Id, Id));
        }

        public long Count()
        {
            return Collection.Count();
        }

        public long Count(IMongoQuery query)
        {
            return Collection.Count(query);
        }

        public void EnsureIndex(IMongoIndexKeys keys)
        {
            Collection.EnsureIndex(keys);
        }

        public void EnsureIndex(IMongoIndexKeys keys, IMongoIndexOptions options)
        {
            Collection.EnsureIndex(keys, options);
        }

        public GetIndexesResult GetIndexes()
        {
            return Collection.GetIndexes();
        }

    }
}
